Skip to content

Do not write a proxy's port into a forwarded https url - #8

Merged
TomK merged 1 commit into
masterfrom
fix-scheme-port-mismatch
Sep 4, 2026
Merged

Do not write a proxy's port into a forwarded https url#8
TomK merged 1 commit into
masterfrom
fix-scheme-port-mismatch

Conversation

@TomK

@TomK TomK commented Sep 4, 2026

Copy link
Copy Markdown
Member

Follow-up to the note on #7. asUrl() resolved the scheme twice, differently: the output used isSecure(true), which honours X-Forwarded-Proto, while the port check was handed getScheme(), which doesn't. Behind a TLS-terminating proxy they disagreed, so the URL claimed https while port suppression was evaluated as http:

Request::create('http://www.packaged.local:443/') + X-Forwarded-Proto: https
  -> https://www.packaged.local:443

Passing the resolved scheme to _isStandardPort() isn't sufficient, which is worth flagging since it's the obvious one-line fix — I tried it and it broke the existing forwarded-proto assertion. A proxy forwards on its own port, usually neither 80 nor 443 (8080 being the common case), so no default-port comparison suppresses it. The port the request arrived on simply isn't part of the public URL.

So when the scheme came from the forwarded header and no port was set explicitly, the port is omitted. Explicit setPort() still wins; non-proxied requests are untouched.

This assumes the public URL is on the scheme's default port, which is all X-Forwarded-Proto tells us. If the public port is non-standard, configure trusted proxies — getPort() then honours X-Forwarded-Port.

Test plan

The existing forwarded-proto assertion only passed because it used port 80, standard for the mis-resolved http scheme. Added the 443 case; it fails without the src change, which I confirmed before fixing.

Full suite green on PHP 8.2.33 and 8.5.10: 45 tests, 161 assertions, exit 0.

Also checked by hand, since tests don't cover all of it:

case result
proxied https, origin :8080 https://host
proxied https, origin :80 / :443 https://host
proxied + explicit setPort(8443) https://host:8443
not proxied, http :81 / :80 http://host:81 / http://host
real https :443 / :8443 https://host / https://host:8443
explicit setScheme('https'), request :81 https://host:81

🤖 Generated with Claude Code

asUrl() resolved the scheme twice, and differently. The output used
isSecure(true), which honours X-Forwarded-Proto, while the port check was
handed getScheme(), which does not. Behind a tls terminating proxy the two
disagreed, so the url claimed https while port suppression was evaluated as
http, leaving the origin's port in place:

    Request::create('http://www.packaged.local:443/') + X-Forwarded-Proto
      -> https://www.packaged.local:443

Passing the resolved scheme to _isStandardPort() alone is not enough. A proxy
forwards on its own port, which is usually neither 80 nor 443 -- 8080 is the
common case -- so no default-port comparison suppresses it. The port the
request arrived on simply is not part of the public url.

So when the scheme came from the forwarded header and no port was set
explicitly, the port is now omitted. An explicit setPort() still wins, and
requests that were not proxied are unaffected.

Note this assumes the public url is on the scheme's default port, which is
all X-Forwarded-Proto tells us. Configure trusted proxies if the public port
is non-standard; getPort() then honours X-Forwarded-Port.

The existing forwarded-proto assertion only passed because it used port 80,
which is standard for the mis-resolved http scheme. Adds the 443 case, which
fails without this change.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@TomK
TomK merged commit 438eef6 into master Sep 4, 2026
4 checks passed
@TomK
TomK deleted the fix-scheme-port-mismatch branch September 4, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant